بر هوک useCallback در ریاکت مسلط شوید تا عملکرد توابع را بهینه کرده، از رندرهای مجدد غیرضروری جلوگیری کنید و اپلیکیشنهای کارآمد و با کارایی بالا بسازید.
React useCallback: مموایز کردن توابع و بهینهسازی وابستگیها
ریاکت یک کتابخانه قدرتمند جاوااسکریپت برای ساخت رابطهای کاربری است و به طور گسترده توسط توسعهدهندگان در سراسر جهان استفاده میشود. یکی از جنبههای کلیدی ساخت اپلیکیشنهای کارآمد در ریاکت، مدیریت رندرهای مجدد کامپوننتها است. رندرهای غیرضروری میتوانند به طور قابل توجهی بر عملکرد تأثیر بگذارند، به ویژه در اپلیکیشنهای پیچیده. ریاکت ابزارهایی مانند useCallback را برای کمک به توسعهدهندگان در بهینهسازی عملکرد توابع و کنترل زمان ایجاد مجدد آنها فراهم میکند و در نتیجه کارایی کلی اپلیکیشن را بهبود میبخشد. این پست وبلاگ به بررسی هوک useCallback میپردازد و هدف، مزایا و نحوه استفاده مؤثر از آن برای بهینهسازی کامپوننتهای ریاکت شما را توضیح میدهد.
useCallback چیست؟
useCallback یک هوک ریاکت است که یک تابع را مموایز (memoize) میکند. مموایزیشن یک تکنیک بهینهسازی عملکرد است که در آن نتایج فراخوانیهای پرهزینه توابع کش (cache) میشوند و فراخوانیهای بعدی تابع، در صورتی که ورودی تغییر نکرده باشد، نتیجه کش شده را برمیگردانند. در زمینه ریاکت، useCallback به جلوگیری از ایجاد مجدد غیرضروری توابع در کامپوننتهای تابعی کمک میکند. این موضوع به ویژه هنگام ارسال توابع به عنوان props به کامپوننتهای فرزند مفید است.
سینتکس اصلی آن به این صورت است:
const memoizedCallback = useCallback(
() => {
// Function logic
},
[dependency1, dependency2, ...]
);
بیایید بخشهای کلیدی را بررسی کنیم:
memoizedCallback: این متغیری است که تابع مموایز شده را نگهداری میکند.useCallback: هوک ریاکت.() => { ... }: این تابعی است که میخواهید مموایز کنید. این تابع شامل منطقی است که قصد اجرای آن را دارید.[dependency1, dependency2, ...]: این یک آرایه از وابستگیها است. تابع مموایز شده تنها در صورتی دوباره ایجاد میشود که هر یک از وابستگیها تغییر کند. اگر آرایه وابستگی خالی باشد ([])، تابع فقط یک بار در رندر اولیه ایجاد میشود و در تمام رندرهای بعدی ثابت باقی میماند.
چرا از useCallback استفاده کنیم؟ مزایا
استفاده از useCallback مزایای متعددی برای بهینهسازی اپلیکیشنهای ریاکت دارد:
- جلوگیری از رندرهای مجدد غیرضروری: مزیت اصلی، جلوگیری از رندر مجدد غیرضروری کامپوننتهای فرزند است. وقتی یک تابع به عنوان prop به یک کامپوننت فرزند ارسال میشود، ریاکت آن را در هر رندر به عنوان یک prop جدید در نظر میگیرد، مگر اینکه تابع را با استفاده از
useCallbackمموایز کنید. اگر تابع دوباره ایجاد شود، کامپوننت فرزند ممکن است حتی اگر سایر props آن تغییر نکرده باشند، دوباره رندر شود. این میتواند یک گلوگاه عملکردی مهم باشد. - بهبود عملکرد: با جلوگیری از رندرهای مجدد،
useCallbackعملکرد کلی اپلیکیشن شما را بهبود میبخشد، به ویژه در سناریوهایی با کامپوننتهای والد که به طور مکرر رندر میشوند و کامپوننتهای فرزند پیچیده. این امر به خصوص در اپلیکیشنهایی که با مجموعه دادههای بزرگ سروکار دارند یا تعاملات مکرر کاربر را مدیریت میکنند، صادق است. - بهینهسازی هوکهای سفارشی:
useCallbackاغلب در هوکهای سفارشی برای مموایز کردن توابعی که توسط هوک بازگردانده میشوند، استفاده میشود. این کار تضمین میکند که توابع تغییر نکنند مگر اینکه وابستگیهایشان تغییر کند، که به جلوگیری از رندرهای مجدد غیرضروری در کامپوننتهایی که از این هوکهای سفارشی استفاده میکنند، کمک میکند. - پایداری و پیشبینیپذیری بهبود یافته: با کنترل زمان ایجاد توابع،
useCallbackمیتواند به رفتار پیشبینیپذیرتر در اپلیکیشن شما کمک کند و شانس اثرات جانبی غیرمنتظره ناشی از توابع دائماً در حال تغییر را کاهش دهد. این برای دیباگ کردن و نگهداری اپلیکیشن مفید است.
useCallback چگونه کار میکند: نگاهی عمیقتر
هنگامی که useCallback فراخوانی میشود، ریاکت بررسی میکند که آیا هیچ یک از وابستگیها در آرایه وابستگی از آخرین رندر تاکنون تغییر کردهاند یا خیر. اگر وابستگیها تغییر نکرده باشند، useCallback تابع مموایز شده از رندر قبلی را برمیگرداند. اگر هر یک از وابستگیها تغییر کرده باشد، useCallback تابع را دوباره ایجاد کرده و تابع جدید را برمیگرداند.
اینطور به آن فکر کنید: تصور کنید یک نوع ماشین فروش خودکار ویژه دارید که تابع توزیع میکند. شما لیستی از مواد اولیه (وابستگیها) را به ماشین میدهید. اگر آن مواد اولیه تغییر نکرده باشند، ماشین همان تابعی را که دفعه قبل گرفتید به شما میدهد. اگر هر یک از مواد اولیه تغییر کند، ماشین یک تابع جدید ایجاد میکند.
مثال:
import React, { useCallback, useState } from 'react';
function ChildComponent({ onClick }) {
console.log('ChildComponent re-rendered');
return (
);
}
function ParentComponent() {
const [count, setCount] = useState(0);
// Without useCallback - this will create a new function on every render!
// const handleClick = () => {
// setCount(count + 1);
// };
// With useCallback - the function only re-creates when 'setCount' changes
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]); // 'count' is the dependency
console.log('ParentComponent re-rendered');
return (
Count: {count}
);
}
export default ParentComponent;
در این مثال، بدون useCallback، تابع handleClick در هر رندر ParentComponent یک تابع جدید خواهد بود. این باعث میشود ChildComponent هر بار که ParentComponent رندر میشود، دوباره رندر شود، حتی اگر خودِ کنترلکننده کلیک تغییر نکرده باشد. با useCallback، تابع handleClick تنها زمانی تغییر میکند که وابستگیهایش تغییر کنند. در این مورد، وابستگی count است که با افزایش شمارنده تغییر میکند.
چه زمانی از useCallback استفاده کنیم: بهترین شیوهها
در حالی که useCallback میتواند ابزار قدرتمندی باشد، مهم است که از آن به صورت استراتژیک استفاده کنید تا از بهینهسازی بیش از حد و پیچیدگی غیرضروری جلوگیری شود. در اینجا راهنمایی برای زمان استفاده و عدم استفاده از آن آورده شده است:
- چه زمانی استفاده کنیم:
- ارسال توابع به عنوان Props به کامپوننتهای مموایز شده: این رایجترین و حیاتیترین مورد استفاده است. اگر یک تابع را به عنوان prop به کامپوننتی که در
React.memoپیچیده شده است (یا با استفاده ازuseMemoمموایز شده است) ارسال میکنید، *باید* ازuseCallbackاستفاده کنید تا از رندر مجدد غیرضروری کامپوننت فرزند جلوگیری شود. این امر به ویژه زمانی مهم است که رندر مجدد کامپوننت فرزند پرهزینه باشد. - بهینهسازی هوکهای سفارشی: مموایز کردن توابع در هوکهای سفارشی برای جلوگیری از ایجاد مجدد آنها مگر اینکه وابستگیها تغییر کنند.
- بخشهای حیاتی از نظر عملکرد: در بخشهایی از اپلیکیشن که عملکرد کاملاً حیاتی است (مثلاً در حلقههایی که کامپوننتهای زیادی را رندر میکنند)، استفاده از
useCallbackمیتواند کارایی را به طور قابل توجهی بهبود بخشد. - توابع استفاده شده در کنترلکنندههای رویداد که ممکن است باعث رندر مجدد شوند: اگر تابعی که به یک کنترلکننده رویداد ارسال میشود مستقیماً بر تغییرات state که میتواند باعث رندر مجدد شود تأثیر بگذارد، استفاده از
useCallbackکمک میکند تا اطمینان حاصل شود که تابع دوباره ایجاد نمیشود و در نتیجه، کامپوننت بیهوده رندر مجدد نمیشود. - چه زمانی استفاده نکنیم:
- کنترلکنندههای رویداد ساده: برای کنترلکنندههای رویداد ساده که مستقیماً بر عملکرد تأثیر نمیگذارند یا با کامپوننتهای فرزند مموایز شده تعامل ندارند، استفاده از
useCallbackممکن است پیچیدگی غیرضروری اضافه کند. بهتر است قبل از استفاده، تأثیر واقعی آن را ارزیابی کنید. - توابعی که به عنوان Props ارسال نمیشوند: اگر یک تابع فقط در محدوده یک کامپوننت استفاده میشود و به کامپوننت فرزند ارسال نمیشود یا به گونهای استفاده نمیشود که باعث رندر مجدد شود، معمولاً نیازی به مموایز کردن آن نیست.
- استفاده بیش از حد: استفاده بیش از حد از
useCallbackمیتواند منجر به کدی شود که خواندن و درک آن دشوارتر است. همیشه توازن بین مزایای عملکرد و خوانایی کد را در نظر بگیرید. پروفایل کردن اپلیکیشن برای یافتن گلوگاههای عملکردی واقعی اغلب اولین قدم است.
درک وابستگیها
آرایه وابستگی برای نحوه کار useCallback حیاتی است. این آرایه به ریاکت میگوید که چه زمانی تابع مموایز شده را دوباره ایجاد کند. مشخص کردن نادرست وابستگیها میتواند منجر به رفتار غیرمنتظره یا حتی باگ شود.
- تمام وابستگیها را شامل شوید: اطمینان حاصل کنید که *تمام* متغیرهای استفاده شده در داخل تابع مموایز شده را در آرایه وابستگی قرار دهید. این شامل متغیرهای state، props و هر مقدار دیگری است که تابع به آن وابسته است. عدم وجود وابستگیها میتواند منجر به «بستارهای کهنه» (stale closures) شود، جایی که تابع از مقادیر قدیمی استفاده میکند و نتایج غیرقابل پیشبینی ایجاد میکند. لینتر ریاکت اغلب در مورد وابستگیهای گمشده به شما هشدار میدهد.
- از وابستگیهای غیرضروری اجتناب کنید: وابستگیهایی را که تابع واقعاً از آنها استفاده نمیکند، شامل نکنید. این میتواند منجر به ایجاد مجدد غیرضروری تابع شود.
- وابستگیها و بهروزرسانیهای State: هنگامی که یک وابستگی تغییر میکند، تابع مموایز شده دوباره ایجاد میشود. اطمینان حاصل کنید که نحوه کار بهروزرسانیهای state خود و ارتباط آنها با وابستگیهایتان را درک میکنید.
- مثال:
import React, { useCallback, useState } from 'react';
function MyComponent({ prop1 }) {
const [stateValue, setStateValue] = useState(0);
const handleClick = useCallback(() => {
// Include all dependencies: prop1 and stateValue
console.log('prop1: ', prop1, 'stateValue: ', stateValue);
setStateValue(stateValue + 1);
}, [prop1, stateValue]); // Correct dependency array
return ;
}
در این مثال، اگر prop1 را از آرایه وابستگی حذف کنید، تابع همیشه از مقدار اولیه prop1 استفاده میکند، که احتمالاً چیزی نیست که شما میخواهید.
useCallback در مقابل useMemo: تفاوت چیست؟
هر دو useCallback و useMemo هوکهای ریاکت هستند که برای مموایزیشن استفاده میشوند، اما اهداف متفاوتی دارند:
useCallback: یک *تابع* مموایز شده را برمیگرداند. برای بهینهسازی توابع با جلوگیری از ایجاد مجدد آنها مگر اینکه وابستگیهایشان تغییر کند، استفاده میشود. عمدتاً برای بهینهسازی عملکرد مربوط به ارجاعات تابع و رندرهای مجدد کامپوننتهای فرزند طراحی شده است.useMemo: یک *مقدار* مموایز شده را برمیگرداند. برای مموایز کردن نتیجه یک محاسبه استفاده میشود. این میتواند برای جلوگیری از اجرای مجدد محاسبات پرهزینه در هر رندر استفاده شود، به ویژه آنهایی که خروجی آنها نیازی به تابع بودن ندارد.
چه زمانی انتخاب کنیم:
- زمانی که میخواهید یک تابع را مموایز کنید، از
useCallbackاستفاده کنید. - زمانی که میخواهید یک مقدار محاسبه شده (مانند یک شیء، یک آرایه یا یک مقدار اولیه) را مموایز کنید، از
useMemoاستفاده کنید.
مثال با useMemo:
import React, { useMemo, useState } from 'react';
function MyComponent({ items }) {
const [filter, setFilter] = useState('');
// Memoize the filtered items - an array is the result
const filteredItems = useMemo(() => {
return items.filter(item => item.includes(filter));
}, [items, filter]);
return (
setFilter(e.target.value)} />
{filteredItems.map(item => (
- {item}
))}
);
}
در این مثال، useMemo آرایه filteredItems را که نتیجه عملیات فیلتر کردن است، مموایز میکند. این هوک فقط زمانی آرایه را دوباره محاسبه میکند که items یا filter تغییر کند. این کار از رندر مجدد غیرضروری لیست هنگام تغییر سایر بخشهای کامپوننت جلوگیری میکند.
React.memo و useCallback: ترکیبی قدرتمند
React.memo یک کامپوننت مرتبه بالا (HOC) است که یک کامپوننت تابعی را مموایز میکند. این HOC از رندرهای مجدد کامپوننت در صورتی که props آن تغییر نکرده باشد، جلوگیری میکند. هنگامی که با useCallback ترکیب شود، قابلیتهای بهینهسازی قدرتمندی به دست میآورید.
- چگونه کار میکند:
React.memoیک مقایسه سطحی (shallow comparison) از props ارسال شده به یک کامپوننت انجام میدهد. اگر props یکسان باشند (طبق مقایسه سطحی)، کامپوننت دوباره رندر نمیشود. اینجاست کهuseCallbackوارد میشود: با مموایز کردن توابعی که به عنوان props ارسال میشوند، اطمینان حاصل میکنید که توابع تغییر نکنند مگر اینکه وابستگیها تغییر کنند. این بهReact.memoاجازه میدهد تا به طور مؤثر از رندرهای مجدد کامپوننت مموایز شده جلوگیری کند. - مثال:
import React, { useCallback } from 'react';
// Memoized child component
const ChildComponent = React.memo(({ onClick, text }) => {
console.log('ChildComponent re-rendered');
return (
);
});
function ParentComponent() {
const [count, setCount] = React.useState(0);
const handleClick = useCallback(() => {
setCount(count + 1);
}, [count]);
return (
Count: {count}
);
}
در این مثال، ChildComponent با React.memo مموایز شده است. prop onClick با استفاده از useCallback مموایز شده است. این تنظیمات تضمین میکند که ChildComponent فقط زمانی دوباره رندر میشود که خود تابع handleClick دوباره ایجاد شود (که تنها زمانی اتفاق میافتد که count تغییر کند) و زمانی که prop text تغییر کند.
تکنیکهای پیشرفته و ملاحظات
فراتر از اصول اولیه، چند تکنیک پیشرفته و ملاحظات وجود دارد که هنگام استفاده از useCallback باید در نظر داشته باشید:
- منطق مقایسه سفارشی با
React.memo: در حالی کهReact.memoبه طور پیشفرض یک مقایسه سطحی از props انجام میدهد، میتوانید یک آرگومان دوم، یعنی یک تابع مقایسه، برای سفارشی کردن مقایسه prop ارائه دهید. این امکان کنترل دقیقتری بر زمان رندر مجدد یک کامپوننت را فراهم میکند. این کار زمانی مفید است که props شما اشیاء پیچیدهای باشند که نیاز به مقایسه عمیق دارند. - ابزارهای پروفایلینگ و عملکرد: از React DevTools و ابزارهای پروفایلینگ مرورگر برای شناسایی گلوگاههای عملکردی در اپلیکیشن خود استفاده کنید. این میتواند به شما کمک کند تا مناطقی را که
useCallbackو سایر تکنیکهای بهینهسازی میتوانند بیشترین سود را داشته باشند، مشخص کنید. ابزارهایی مانند React Profiler در Chrome DevTools میتوانند به صورت بصری به شما نشان دهند که کدام کامپوننتها و چرا دوباره رندر میشوند. - از بهینهسازی زودهنگام اجتناب کنید: استفاده از
useCallbackرا در همه جای اپلیکیشن خود شروع نکنید. ابتدا، اپلیکیشن خود را برای شناسایی گلوگاههای عملکردی پروفایل کنید. سپس، بر روی بهینهسازی کامپوننتهایی که بیشترین مشکلات را ایجاد میکنند، تمرکز کنید. بهینهسازی زودهنگام میتواند منجر به کد پیچیدهتر بدون افزایش قابل توجه عملکرد شود. - جایگزینها را در نظر بگیرید: در برخی موارد، تکنیکهای دیگری مانند code splitting، lazy loading و virtualization ممکن است برای بهبود عملکرد مناسبتر از استفاده از
useCallbackباشند. هنگام تصمیمگیری برای بهینهسازی، معماری کلی اپلیکیشن خود را در نظر بگیرید. - بهروزرسانی وابستگیها: هنگامی که یک وابستگی تغییر میکند، تابع مموایز شده دوباره ایجاد میشود. این میتواند منجر به مشکلات عملکردی شود اگر تابع عملیات پرهزینهای انجام دهد. تأثیر وابستگیهای خود و تعداد دفعات تغییر آنها را به دقت در نظر بگیرید. گاهی اوقات، بازنگری در طراحی کامپوننت یا استفاده از یک رویکرد متفاوت ممکن است کارآمدتر باشد.
مثالهای واقعی و کاربردهای جهانی
useCallback به طور گسترده در اپلیکیشنهای ریاکت با هر اندازهای، از پروژههای شخصی کوچک گرفته تا اپلیکیشنهای سازمانی در مقیاس بزرگ، استفاده میشود. در اینجا چند سناریوی واقعی و نحوه کاربرد useCallback آورده شده است:
- پلتفرمهای تجارت الکترونیک: در اپلیکیشنهای تجارت الکترونیک، میتوان از
useCallbackبرای بهینهسازی عملکرد کامپوننتهای لیست محصولات استفاده کرد. هنگامی که کاربر با لیست محصولات تعامل میکند (مثلاً فیلتر کردن، مرتبسازی)، رندرها باید کارآمد باشند تا تجربه کاربری روانی حفظ شود. مموایز کردن توابع کنترلکننده رویداد (مانند افزودن یک مورد به سبد خرید) که به کامپوننتهای فرزند ارسال میشوند، تضمین میکند که آن کامپوننتها بیهوده رندر مجدد نشوند. - اپلیکیشنهای رسانههای اجتماعی: پلتفرمهای رسانههای اجتماعی اغلب دارای رابطهای کاربری پیچیده با کامپوننتهای متعدد هستند.
useCallbackمیتواند کامپوننتهای نمایشدهنده فیدهای کاربر، بخشهای نظرات و سایر عناصر تعاملی را بهینه کند. کامپوننتی را تصور کنید که لیستی از نظرات را نمایش میدهد. با مموایز کردن تابع `likeComment`، میتوانید از رندر مجدد کل لیست نظرات هر بار که یک کاربر نظری را لایک میکند، جلوگیری کنید. - تجسم دادههای تعاملی: در اپلیکیشنهایی که مجموعه دادههای بزرگ و تجسمها را نمایش میدهند،
useCallbackمیتواند ابزاری کلیدی برای حفظ پاسخگویی باشد. بهینهسازی عملکرد کنترلکنندههای رویداد که برای تعامل با تجسم استفاده میشوند (مثلاً بزرگنمایی، جابجایی، انتخاب نقاط داده) از رندر مجدد کامپوننتهایی که مستقیماً تحت تأثیر تعامل قرار نمیگیرند، جلوگیری میکند. به عنوان مثال، در داشبوردهای مالی یا ابزارهای تحلیل دادههای علمی. - اپلیکیشنهای بینالمللی (بومیسازی و جهانیسازی): در اپلیکیشنهایی که از چندین زبان پشتیبانی میکنند (مانند اپلیکیشنهای ترجمه یا پلتفرمهایی با پایگاههای کاربری بینالمللی)، میتوان از
useCallbackدر کنار کتابخانههای بومیسازی برای جلوگیری از رندرهای مجدد غیرضروری هنگام تغییر زبان استفاده کرد. با مموایز کردن توابع مربوط به واکشی رشتههای ترجمه شده یا قالببندی تاریخها و اعداد، میتوانید اطمینان حاصل کنید که فقط کامپوننتهای تحت تأثیر هنگام تغییر محلی (locale) بهروز میشوند. یک اپلیکیشن بانکداری جهانی را در نظر بگیرید که موجودی حسابها را به ارزهای مختلف نمایش میدهد. اگر ارز تغییر کند، شما فقط میخواهید کامپوننتی را که موجودی را به ارز جدید نمایش میدهد دوباره رندر کنید، نه کل اپلیکیشن را. - سیستمهای احراز هویت و مجوزدهی کاربر: اپلیکیشنهای دارای احراز هویت کاربر (در انواع کشورها، از آمریکا گرفته تا هند و ژاپن، و بسیاری دیگر!) به طور مکرر از کامپوننتهایی استفاده میکنند که جلسات و نقشهای کاربر را مدیریت میکنند. استفاده از
useCallbackبرای مموایز کردن توابع مربوط به ورود، خروج و بهروزرسانی مجوزهای کاربر، تضمین میکند که رابط کاربری به طور کارآمد پاسخ دهد. هنگامی که یک کاربر وارد میشود یا نقش او تغییر میکند، فقط کامپوننتهای تحت تأثیر نیاز به رندر مجدد دارند.
نتیجهگیری: تسلط بر useCallback برای توسعه کارآمد در ریاکت
useCallback یک ابزار حیاتی برای توسعهدهندگان ریاکت است که به دنبال بهینهسازی اپلیکیشنهای خود هستند. با درک هدف، مزایا و نحوه استفاده مؤثر از آن، میتوانید عملکرد کامپوننتهای خود را به طور قابل توجهی بهبود بخشید، رندرهای مجدد غیرضروری را کاهش دهید و تجربه کاربری روانتری ایجاد کنید. به یاد داشته باشید که از آن به صورت استراتژیک استفاده کنید، اپلیکیشن خود را برای شناسایی گلوگاهها پروفایل کنید و آن را با سایر تکنیکهای بهینهسازی مانند React.memo و useMemo ترکیب کنید تا اپلیکیشنهای ریاکت کارآمد و قابل نگهداری بسازید.
با پیروی از بهترین شیوهها و مثالهای ذکر شده در این پست وبلاگ، شما به خوبی برای بهرهبرداری از قدرت useCallback و نوشتن اپلیکیشنهای ریاکت با عملکرد بالا برای مخاطبان جهانی مجهز خواهید شد.